﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Innovative.Atom.Timer.Single;

namespace Innovative.Atom.Timer.Event
{
	public class AtomEventTimer
	{
		private bool _initialized = false;
		private AtomSingleTimer _atomSingleTimer = new AtomSingleTimer();
		private ObservableCollection<IAtomEvent> _eventList = new ObservableCollection<IAtomEvent>();
		private long _eventCounter = 0;
		private IAtomEvent _cachedNextEvent = null;
		private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();

		public AtomEventTimer()
		{
			this.EventList.CollectionChanged += _EventList_CollectionChanged;
		}		

		public ObservableCollection<IAtomEvent> EventList
		{
			get
			{
				return _eventList;
			}
		}

		public IAtomEvent NextEvent
		{
			get
			{
				IAtomEvent returnValue = null;

				if (this.EventList.Count > 0)
				{
					// ***
					// *** Take a snapshot of the items
					// ***
					IAtomEvent[] items = this.EventList.ToArray();

					// ***
					// *** Get the next atom event
					// ***
					returnValue = items.Where(t => !t.TaskIsExpired).OrderBy(t => t.EventDateTime).First();
				}

				return returnValue;
			}
		}	

		private IAtomEvent _CachedNextEvent
		{
			get
			{
				if (_cachedNextEvent == null)
				{
					_cachedNextEvent = this.NextEvent;
				}

				return _cachedNextEvent;
			}
			set
			{
				_cachedNextEvent = value;
			}
		}

		private void _EventList_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
		{
			switch (e.Action)
			{
				case NotifyCollectionChangedAction.Add:
				case NotifyCollectionChangedAction.Replace:
					foreach (IAtomEvent item in e.NewItems)
					{
						item.InitializeNextEventDateTime();
					}

					// ***
					// *** A new item may come prior to the current
					// *** 'Next' event. Reset the timer to ensure
					// *** the items properly ordered.
					// ***
					this._ResetTimer();
					break;
				case NotifyCollectionChangedAction.Remove:
				case NotifyCollectionChangedAction.Reset:
					// ***
					// *** The item removed may be the 'Next' 
					// *** event. Reset the timer to ensure
					// *** the items properly ordered.
					// ***
					this._ResetTimer();
					break;
			}
		}

		private void _AtomSingleTimer_Task(IAtomEvent atomEvent)
		{
			try
			{
				// ***
				// *** Ensure the state contained a valid object
				// ***
				if (atomEvent != null)
				{
					Interlocked.Increment(ref _eventCounter);
					Trace.TraceInformation("Atom Timer Callback {0} fired.", _eventCounter);

					lock (_atomSingleTimer.SyncRoot)
					{
						// ***
						// *** Create the event history data
						// ***
						AtomEventData eventData = new AtomEventData();
						eventData.EventId = atomEvent.EventId;
						eventData.EventName = atomEvent.EventName;
						eventData.ExpectedDateTime = atomEvent.EventDateTime;
						eventData.ActualDateTime = DateTime.Now;

						// ***
						// *** Request an update for the event
						// ***
						atomEvent.UpdateNextEventDateTime();

						lock (_eventList)
						{
							// ***
							// *** Check if this event is expired. If it is remove it
							// *** from the list. If it isn't, the due time of this
							// *** event will automatically adjust itself to the
							// *** next event date and time which will cause it to be
							// *** reordered in the list
							// ***
							if (atomEvent.TaskIsExpired)
							{
								// ***
								// *** Remove this event from the list
								// ***
								this.EventList.Remove(atomEvent);
							}
						}

						// ***
						// *** Start the event as starting
						// ***
						atomEvent.TaskStarting(eventData);

						try
						{
							// ***
							// *** Execute the task
							// ***
							if (atomEvent.EventTask != null && atomEvent.EventTask.Status == TaskStatus.Created)
							{
								atomEvent.EventTask.Start();
							}
							else
							{
								Trace.TraceWarning("Attempted to start a task that has already been started.");
							}
						}
						catch (Exception ex)
						{
							atomEvent.TaskError(new AtomTaskExecutionException(ex));
						}
						finally
						{
							// ***
							// *** Once a task has been executed it cannot
							// *** be run again. This call gives the event object
							// *** the opportunity to create a new task for the
							// *** next event.
							// ***
							atomEvent.RequestNextTask();

							// ***
							// *** Signal the event completed
							// ***
							atomEvent.TaskCompleted();
						}
					}
				}
			}
			catch (Exception ex)
			{
				// ***
				// *** Raise the exception event
				// ***
				if (atomEvent != null)
				{
					atomEvent.TaskError(new AtomTaskExecutionException(ex));
				}
				else
				{
					Trace.TraceError("Exception: '{0}'.", ex.Message);
				}
			}
			finally
			{
				// ***
				// *** Clear the timer
				// ***
				this._ClearTimer();

				// ***
				// *** Set the timer to collect the next event
				// ***
				this._SetNextEvent();
			}
		}		

		private void _ResetTimer()
		{
			lock (_atomSingleTimer.SyncRoot)
			{
				// ***
				// *** Make a couple of checks before resetting the timer:
				// *** 1) First time
				// *** 2) Next Event and the Cache Next event are different (indicating a change
				// ***    such as a added or removed event
				// ***
				if (!_initialized || (this._CachedNextEvent != null && this.NextEvent != null && this._CachedNextEvent != this.NextEvent))
				{
					// ***
					// *** Set the _initialized flag to true so this will
					// *** only be run when there are changes to the event list
					// ***
					_initialized = true;

					// ***
					// *** Clear the timer
					// ***
					this._ClearTimer();

					// ***
					// *** Trigger an event without a state object; this will cause the first
					// *** event to load.
					// ***
					AtomSingleTimer eventItem = new AtomSingleTimer()
					{
						DueTime = TimeSpan.FromMilliseconds(50),						
						EventTask = new Task(() => { this._AtomSingleTimer_Task(null); }),
						ThreadPriority = ThreadPriority.AboveNormal,
					};

					// ***
					// *** Set the cancellation source
					// ***
					this._ResetCancellationSource();

					// ***
					// *** Start the timer
					// ***
					eventItem.Start(_cancellationTokenSource.Token);
				}
			}
		}

		private void _SetNextEvent()
		{
			lock (_atomSingleTimer.SyncRoot)
			{
				// ***
				// *** Set the timer if there is a next event and the timer
				// *** is not currently set for an event
				// ***
				if (this.NextEvent != null && !this._atomSingleTimer.IsActive)
				{
					// ***
					// *** Cache the next event to detect changes in the list
					// ***
					this._CachedNextEvent = this.NextEvent;

					// ***
					// *** Set the timer to in the amount of time necessary for the next event
					// ***
					AtomSingleTimer eventItem = new AtomSingleTimer()
					{
						DueTime = this.NextEvent.TaskDueTime,
						EventTask = new Task(() => { this._AtomSingleTimer_Task(this.NextEvent); }),
						ThreadPriority = ThreadPriority.Highest,
					};

					// ***
					// *** Set the cancellation source
					// ***
					this._ResetCancellationSource();

					// ***
					// *** Start the timer
					// ***
					eventItem.Start(_cancellationTokenSource.Token);

					Trace.TraceInformation("The next '{0}' event will occur on {1}.", this.NextEvent.EventName, this.NextEvent.EventDateTime.ToFullFormat());
				}
			}
		}

		private void _ClearTimer()
		{
			lock (_atomSingleTimer.SyncRoot)
			{
				if (!_cancellationTokenSource.IsCancellationRequested)
				{
					_cancellationTokenSource.Cancel();
				}
			}
		}

		private void _ResetCancellationSource()
		{
			if (_cancellationTokenSource != null)
			{
				_cancellationTokenSource.Dispose();
				_cancellationTokenSource = new CancellationTokenSource();
			}
		}
	}
}
